<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>
        body {
            margin: 2em;
        }

        table,
        tr,
        th,
        td {
            border: 1px solid rgba(128, 128, 128, 0.8);
            border-collapse: collapse;
            box-sizing: border-box;
            padding: 0.5em;
            text-align: center;
        }

        .section {
            margin-top: 2em;
        }

        .mt-4 {
            margin-top: 1rem;
        }
    </style>
</head>
<body>
    <fieldset style="width: 20rem;">
        <legend>文件上传</legend>
        <form class="form-upload">
            <div class="mt-4">
                <input type="file" name="file" id="file"></input>
                <input type="submit" value="上传">
            </div>
            <br>
            <label for="" class="mt-4">
                上传进度：
                <progress class="progress-upload" max="100" value="0"></progress>
                <span class="progress-upload-text">0</span>
            </label>
        </form>
    </fieldset>


    <section class="section">
        <template id="template-file-row">
            <tr>
                <td>文件1</td>
                <td>100MB</td>
                <td>2024-11-8</td>
                <td>
                    <a href="/files/文件名" download="文件名">下载</a>
                </td>
            </tr>
        </template>

        <table>
            <thead>
                <tr>
                    <th>文件名</th>
                    <th>文件大小</th>
                    <th>上传日期</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
            </tbody>
        </table>
    </section>

    <script src="js/md5.min.js"></script>
    <script>
        function sleep(ms) {
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve();
                }, 1000);
            });
        }

        function BSize2MBSize(size) {
            return size / 1024 / 1024;
        }
        function KBSize2MBSize(size) {
            return size / 1024;
        }
        function MBSize2BSize(size) {
            return size * 1024 * 1024;
        }

        function updateFileList() {
            const rowTemplate = document.querySelector('#template-file-row');
            const tbody = document.querySelector('tbody');
            tbody.innerHTML = "";
            Promise
            .resolve()
            .then(() => {
                fetch("/files", { method: "GET" })
                .then(response => response.json())
                .then((result) => {
                    for (const info of result.data) {
                        const row = rowTemplate.content.cloneNode(true);
                        const [ne, se, te] = row.querySelectorAll('td');
                        const downloadElement = row.querySelector("a");
                        ne.textContent = info.name;
                        se.textContent = BSize2MBSize(info.size).toFixed(2) + "MB";
                        te.textContent = new Date(info.time).toLocaleDateString();
                        downloadElement.href = `/file/${info.md5}@${info.name}`;
                        downloadElement.download = info.name;
                        tbody.appendChild(row);
                    }
                });
            });
        }
        updateFileList();

        function readFileAsArrayBuffer(blob) {
            return new Promise((resolve) => {
                const reader = new FileReader(blob)
                reader.onload = () => {
                    resolve(reader.result);
                };
                reader.readAsArrayBuffer(blob);
            });
        }

        const chunkSize = MBSize2BSize(100); // 1MB
        const maxUploadChunk = 10;
        const progressElement = document.querySelector('.progress-upload');
        const progressTextElement = document.querySelector('.progress-upload-text');
        const uploadForm = document.querySelector('.form-upload');
        uploadForm.onsubmit = async (e) => {
            e.preventDefault();
            progressElement.value = 0;
            progressTextElement.textContent = "0";
            const file = uploadForm.file.files[0];
            if (!file) {
                return alert("请选择文件");
            }
            console.log("上传：", file);

            // 预加载文件
            console.log("预加载文件");
            progressTextElement.textContent = "预加载文件";
            const fileArrayBuffer = await readFileAsArrayBuffer(file);
            const fileMD5 = md5(fileArrayBuffer);
            console.log("文件MD5：", fileMD5);
            progressTextElement.textContent = "0";

            // 分片
            const total = Math.ceil(fileArrayBuffer.byteLength / chunkSize);
            const chunkProgress = 100 / total;
            let progress = 0;
            let index = 0;
            let start = 0;
            let isLastChunk = false;
            let finish = false;
            let uploadingCount = 0;
            let uploadedCount = 0;
            for (let i = 0; i < total; i++) {
                if (finish) {
                    break;
                }
                const end = start + chunkSize;
                const chunk = fileArrayBuffer.slice(start, end);
                start = end;
                if (i >= total - 1) {
                    isLastChunk = true;
                }

                // 上传分片
                const formData = new FormData();
                formData.append("name", file.name);
                formData.append("index", ++index);
                formData.append("total", total);
                formData.append("chunk", new Blob([chunk]));
                formData.append("isLastChunk", isLastChunk);
                formData.append("fileMD5", fileMD5);
                // 等待第一个
                if (index > 1 && uploadedCount < 1) {
                    await sleep(5000);
                }
                while (uploadingCount >= maxUploadChunk) {
                    await sleep(500);
                }
                while (uploadingCount !== 0 && isLastChunk) {
                    await sleep(500);
                }
                uploadingCount++;
                if (finish) {
                    return;
                }
                const xhr = new XMLHttpRequest();
                xhr.open("POST", "/upload/chunk");
                xhr.send(formData);

                function updateProgress(e) {
                    const currentChunkProgress = e.loaded / e.total * chunkProgress;
                    const progress = chunkProgress * uploadedCount + currentChunkProgress
                    progressElement.value = progress;
                    progressTextElement.textContent = progressElement.value.toFixed(2);
                }
                xhr.onprogress = updateProgress;
                xhr.onload = (e) => {
                    if (finish) {
                        return;
                    }
                    uploadingCount--;
                    uploadedCount++;
                    updateProgress(e);
                    const result = JSON.parse(xhr.response);
                    console.log("分片上传结果：", result);
                    if (
                        (result.data && result.data.finish) ||
                        uploadedCount >= total
                    ) {
                        progressElement.value = 100;
                        progressTextElement.textContent = progressElement.value.toFixed(2);
                        finish = true;
                        updateFileList();
                        alert("上传完毕");
                        return;
                    }
                };
            }
        };
    </script>
</body>
</html>

